﻿// -----------------------------------------------------------------------
// <copyright file="BookService.cs" company="EPAM Systems">
////Copyright (c) EPAM Systems, 2012. All rights reserved.
// </copyright>
// -----------------------------------------------------------------------

namespace MBOP.Domain.Services
{
	#region Usings

	using System;
	using System.Collections.Generic;
	using System.Linq;

	using MBOP.Core.Entities.Common;
	using MBOP.Core.Entities.Library;
	using MBOP.Core.FilterContexts;
	using MBOP.Core.Interfaces.Common;
	using MBOP.Core.Interfaces.DataProviders;
	using MBOP.Core.Interfaces.Managers;
	using MBOP.Core.Interfaces.Services;
	using MBOP.Domain.Factories;

	#endregion

	/// <summary>
	/// The <see cref="IBookService"/> implementer
	/// which handles book's operations.
	/// </summary>
	public class BookService : IBookService
	{
		#region Private Fields
		/// <summary>
		/// Represents the book data provider.
		/// </summary>
		private IBookDataProvider _bookDataProvider;

		/// <summary>
		/// Represents the image service.
		/// </summary>
		private IImageService _imageService;

		/// <summary>
		/// Represents the cache manager.
		/// </summary>
		private ICacheManager _cacheManager;

		#endregion

		#region Constructors

		/// <summary>
		/// Initializes a new instance of the <see cref="BookService"/> class.
		/// </summary>
		/// <param name="bookDataProvider">The <see cref="IBookDataProvider"/> implementer</param>
		/// <param name="imageService">The <see cref="IImageService"/> implementer</param>
		/// <param name="cacheManager">The <see cref="ICacheManager"/> implementer</param>
		public BookService(IBookDataProvider bookDataProvider, IImageService imageService, ICacheManager cacheManager)
		{
			this._bookDataProvider = bookDataProvider;
			this._imageService = imageService;
			this._cacheManager = cacheManager;
		}

		#endregion

		#region IBookService Implementation

		#region Methods

		/// <summary>
		/// Adds the book to library
		/// </summary>
		/// <param name="book">The book.</param>
		public void AddBook(Book book)
		{
			if (book != null)
			{
				if (this._cacheManager.IsInCache(book.ImageID))
				{
					book.Image = this._cacheManager.GetItem<Image>(book.ImageID);
				}

				this._bookDataProvider.AddBook(book);
			}
		}

		/// <summary>
		/// Deletes the book from library
		/// </summary>
		/// <param name="bookID">The book id.</param>
		public void DeleteBook(int bookID)
		{
			this._bookDataProvider.DeleteBook(bookID);
		}

		/// <summary>
		/// Updates the book in library
		/// </summary>
		/// <param name="book">The book.</param>
		public void UpdateBook(Book book)
		{
			if (book != null)
			{
				book.Image = this._imageService.GetImage(book.ImageID);

				this._bookDataProvider.UpdateBook(book);
			}
		}

		/// <summary>
		/// Gets the count of books in library by the specific filter context
		/// </summary>
		/// <param name="filterContext">The book filter context.</param>
		/// <returns>The count of elements in the sequence.</returns>
		public int GetBookCount(BookFilterContext filterContext)
		{
			IFilter<Book> bookFilter = BookFilterSimpleFactory.CreateBookFilter(filterContext);

			return this._bookDataProvider.GetBookCount(bookFilter);
		}

		/// <summary>
		/// Get books from library.
		/// </summary>
		/// <param name="skipCount">The number of skipping books.</param>
		/// <param name="takeCount">The number of taking books.</param>
		/// <param name="filterContext">The book filter.</param>\
		/// <param name="sortInfo">The sort info.</param>
		/// <returns>The books collection.</returns>
		public IEnumerable<Book> GetBooks(int skipCount, int takeCount, BookFilterContext filterContext, SortInfo sortInfo)
		{
			IFilter<Book> filter = BookFilterSimpleFactory.CreateBookFilter(filterContext);

			return this._bookDataProvider.GetBooks(skipCount, takeCount, filter, sortInfo);
		}

		/// <summary>
		/// Gets the book from library by id.
		/// </summary>
		/// <param name="bookID">The book id.</param>
		/// <returns>The book.</returns>
		public Book GetBookByID(int bookID)
		{
			return this._bookDataProvider.GetBookByID(bookID);
		}

		/// <summary>
		/// Checks that the current book is available for reading or not.
		/// </summary>
		/// <param name="book">The current book.</param>
		/// <returns>The result of operation.</returns>
		public bool IsAvailableForReading(Book book)
		{
			bool isAvailableForReading = false;

			if (book != null)
			{
				isAvailableForReading = this.GetReader(book) == null;
			}

			return isAvailableForReading;
		}

		/// <summary>
		/// Adds the subscription to book in library.
		/// </summary>
		/// <param name="newBookSubscription">The new book subscription.</param>
		public void AddSubscription(BookSubscription newBookSubscription)
		{
			Book book = this._bookDataProvider.GetBookByID(newBookSubscription.BookID);

			if (book != null)
			{
				BookSubscription currentReader = this.GetReader(book);

				newBookSubscription.SubscriptionSet = ChooseCorrectSet(book, newBookSubscription);

				if (newBookSubscription.State == (int)SubscribeState.Reader && currentReader != null)
				{
					BookSubscription historyReader = new BookSubscription()
						{
							BookID = currentReader.BookID,
							Date = newBookSubscription.Date,
							PersonID = currentReader.PersonID,
							ID = 0,
							State = (int)SubscribeState.History,
							SubscriptionSet = currentReader.SubscriptionSet
						};

					this._bookDataProvider.AddSubscription(historyReader);
				}

				if (newBookSubscription.SubscriptionSet != null)
				{
					this._bookDataProvider.AddSubscription(newBookSubscription);
				}
			}
		}

		/// <summary>
		/// Rolls the subscription to it's previous or next state.
		/// </summary>
		/// <param name="currentBookSubscription">The current person subscription</param>
		/// <param name="isUndo">The value indicates that is undo or redo operation</param>
		public void RollSubscription(BookSubscription currentBookSubscription, bool isUndo)
		{
			if (currentBookSubscription != null)
			{
				Book book = this.GetBookByID(currentBookSubscription.BookID);

				if (book != null)
				{
					BookSubscription rollSubscription = this.GetRollSubscription(book, currentBookSubscription, isUndo);

					if (rollSubscription != null)
					{
						this.AddSubscription(rollSubscription);
					}
				}
			}
		}

		/// <summary>
		/// Deletes all person subscription from book in library.
		/// </summary>
		/// <param name="subscriptionSet">The subscription set.</param>
		/// <param name="bookID">The book id.</param>
		public void DeletePersonSubscriptions(string subscriptionSet, int bookID)
		{
			if (!string.IsNullOrEmpty(subscriptionSet))
			{
				this._bookDataProvider.DeletePersonSubscriptions(subscriptionSet, bookID);
			}
		}

		/// <summary>
		/// Gets the current person's subscription.
		/// </summary>
		/// <param name="book">The book.</param>
		/// <param name="personID">The person's id.</param>
		/// <param name="subscriptionSet">The subscription set</param>
		/// <returns>The book subscription.</returns>
		public BookSubscription GetCurrentPersonSubscription(Book book, string personID, string subscriptionSet)
		{
			BookSubscription currentPersonSubscription = null;

			if (!string.IsNullOrEmpty(subscriptionSet) && !string.IsNullOrEmpty(personID))
			{
				currentPersonSubscription = GetCurrentPersonSubscription(book.Subscriptions, personID, subscriptionSet);
			}
			else
			{
				currentPersonSubscription = GetCurrentPersonSubscription(book.Subscriptions, personID);
			}

			return currentPersonSubscription;
		}

		/// <summary>
		/// Gets the person state time
		/// </summary>
		/// <param name="book">The book where time calculating.</param>
		/// <param name="personID">The person's id.</param>
		/// <param name="subscriptionSet">The subscription set.</param>
		/// <returns>The person state time.</returns>
		public TimeSpan GetPersonTime(Book book, string personID, string subscriptionSet)
		{
			BookSubscription lastPersonState = null;

			if (book != null)
			{
				lastPersonState = GetCurrentPersonSubscription(book, personID, subscriptionSet);
			}

			TimeSpan personStateTime = TimeSpan.Zero;

			if (lastPersonState != null)
			{
				switch (lastPersonState.State)
				{
					case (int)SubscribeState.Reader:
						{
							personStateTime = DateTime.Now.Subtract(lastPersonState.Date);

							break;
						}

					case (int)SubscribeState.History:
						{
							personStateTime = GetReadingTime(book.Subscriptions, personID, subscriptionSet);

							break;
						}

					case (int)SubscribeState.Waiting:
						{
							personStateTime = DateTime.Now.Subtract(lastPersonState.Date);

							break;
						}
				}
			}

			return personStateTime;
		}

		/// <summary>
		/// Gets the current person subscription
		/// </summary>
		/// <param name="book">The current book.</param>
		/// <param name="subscriptionFromBook">Any person subscription for book where to find.</param>
		/// <returns>The current person subscription.</returns>
		public BookSubscription GetCurrentPersonSubscription(Book book, BookSubscription subscriptionFromBook)
		{
			BookSubscription currentPersonSubscription = null;

			if (book != null)
			{
				currentPersonSubscription = GetCurrentPersonSubscription(book, subscriptionFromBook.PersonID, subscriptionFromBook.SubscriptionSet);
			}

			return currentPersonSubscription;
		}

		/// <summary>
		/// Gets the person's id by subscription set.
		/// </summary>
		/// <param name="bookID">The book id</param>
		/// <param name="subscriptionSet">The subscription set.</param>
		/// <returns>The person's id.</returns>
		public string GetPersonIDBySet(int bookID, string subscriptionSet)
		{
			Book book = this._bookDataProvider.GetBookByID(bookID);

			string personID = string.Empty;

			if (book != null)
			{
				BookSubscription subscription =
					book.Subscriptions.FirstOrDefault(subscriptions => subscriptions.SubscriptionSet == subscriptionSet);

				if (subscription != null)
				{
					personID = subscription.PersonID;
				}
			}

			return personID;
		}

		/// <summary>
		/// Gets the current reader.
		/// </summary>
		/// <param name="book">The book.</param>
		/// <returns>The book subscription.</returns>
		public BookSubscription GetReader(Book book)
		{
			BookSubscription reader = null;

			if (book != null)
			{
				reader = book.Subscriptions.Select(subscription => subscription)
					.GroupBy(subscription => subscription.SubscriptionSet)
					.Select(subscription => subscription.Last())
					.Where(subscription => subscription.State == (int)SubscribeState.Reader)
					.OrderByDescending(subscription => subscription.ID)
					.FirstOrDefault();
			}

			return reader;
		}

		/// <summary>
		/// Gets the book's history list.
		/// </summary>
		/// <param name="book">The book.</param>
		/// <returns>The book subscriptions collection.</returns>
		public IEnumerable<BookSubscription> GetHistoryList(Book book)
		{
			IEnumerable<BookSubscription> historyList = new List<BookSubscription>();

			if (book != null)
			{
				historyList = book.Subscriptions.Select(subscription => subscription)
					.GroupBy(subscription => subscription.SubscriptionSet)
					.Select(subscription => subscription.Last())
					.Where(subscription => subscription.State == (int)SubscribeState.History)
					.OrderBy(subscription => subscription.State).ThenBy(subscription => subscription.Date);
			}

			return historyList;
		}

		/// <summary>
		/// Gets the book's waiting list.
		/// </summary>
		/// <param name="book">The book.</param>
		/// <returns>The book subscriptions collection</returns>
		public IEnumerable<BookSubscription> GetWaitingList(Book book)
		{
			IEnumerable<BookSubscription> waitingList = new List<BookSubscription>();

			if (book != null)
			{
				waitingList = book.Subscriptions.Select(subscription => subscription)
					.GroupBy(subscription => subscription.SubscriptionSet)
					.Select(selector => selector.Last())
					.Where(subscription => subscription.State == (int)SubscribeState.Waiting)
					.OrderBy(subscription => subscription.Date);
			}

			return waitingList;
		}

		/// <summary>
		/// Checks the current person with specified identity already is in waiting list or not.
		/// </summary>
		/// <param name="book">The book.</param>
		/// <param name="personID">The person's id.</param>
		/// <returns>The result of checking operation.</returns>
		public bool IsInWaitingList(Book book, string personID)
		{
			return this.GetWaitingList(book).FirstOrDefault(bookSubscription => bookSubscription.PersonID == personID) != null;
		}

		/// <summary>
		/// Checks the current person with specified indentity is already  in history list or not.
		/// </summary>
		/// <param name="book">The book.</param>
		/// <param name="personID">The person's id.</param>
		/// <returns>The result of checking operation.</returns>
		public bool IsInHistoryList(Book book, string personID)
		{
			return this.GetHistoryList(book).FirstOrDefault(bookSubscription => bookSubscription.PersonID == personID) != null;
		}

		/// <summary>
		/// Checks the current person can be added into the waiting list or not.
		/// </summary>
		/// <param name="book">The current book.</param>
		/// <param name="personID">The person id.</param>
		/// <returns>The result of the checking operation.</returns>
		public bool CanAddInWaitingList(Book book, string personID)
		{
			IEnumerable<BookSubscription> waitingList = this.GetWaitingList(book);
			BookSubscription reader = this.GetReader(book);

			return waitingList.FirstOrDefault(subscription => subscription.PersonID == personID) == null
					&& (reader == null || reader.PersonID != personID);
		}

		#endregion

		#endregion

		#region Private Methods

		/// <summary>
		/// Checks the subscription sets are equal or not.
		/// </summary>
		/// <param name="firstSubscriptionSet">The first subscription set.</param>
		/// <param name="secondSubscriptionSet">The second subscription set.</param>
		/// <returns>The result of checking operation</returns>
		private static bool IsSubscriptionSetEquals(string firstSubscriptionSet, string secondSubscriptionSet)
		{
			bool isSetEquals = false;

			if (!string.IsNullOrEmpty(firstSubscriptionSet) && !string.IsNullOrEmpty(secondSubscriptionSet))
			{
				isSetEquals = firstSubscriptionSet == secondSubscriptionSet;
			}

			return isSetEquals;
		}

		/// <summary>
		/// Gets the current person subscription.
		/// </summary>
		/// <param name="subscriptions">The book subscriptions, where to search.</param>
		/// <param name="personID">The person's id.</param>
		/// <param name="subscriptionSet">The subscription set.</param>
		/// <returns>The book subscription.</returns>
		private static BookSubscription GetCurrentPersonSubscription(IEnumerable<BookSubscription> subscriptions, string personID, string subscriptionSet)
		{
			BookSubscription currentSubscription = subscriptions.Select(subscription => subscription)
				.Where(subscription => subscription.PersonID == personID
				&& IsSubscriptionSetEquals(subscription.SubscriptionSet, subscriptionSet))
				.OrderByDescending(subscription => subscription.ID)
				.FirstOrDefault();

			return currentSubscription;
		}

		/// <summary>
		/// Gets the person reading time.
		/// </summary>
		/// <param name="subscriptions">The book subscriptions.</param>
		/// <param name="personID">The person's id.</param>
		/// <param name="subscriptionSet">The subscription set.</param>
		/// <returns>The person reading time.</returns>
		private static TimeSpan GetReadingTime(IEnumerable<BookSubscription> subscriptions, string personID, string subscriptionSet)
		{
			List<BookSubscription> personSubscriptions = subscriptions.Where(subscription
														  =>
														  subscription.PersonID == personID
														  && subscription.SubscriptionSet == subscriptionSet)
														  .OrderByDescending(subscription => subscription.ID)
														  .ToList();

			int readingStateIndex = personSubscriptions.FindIndex(subscription => subscription.State == (int)SubscribeState.Reader);

			TimeSpan readingTime = TimeSpan.Zero;

			if (readingStateIndex != -1)
			{
				readingTime = personSubscriptions[readingStateIndex - 1].Date.Subtract(personSubscriptions[readingStateIndex].Date);

				if (readingTime < TimeSpan.Zero)
				{
					readingTime = TimeSpan.Zero;
				}
			}

			return readingTime;
		}

		/// <summary>
		/// Gets the current person subscription.
		/// </summary>
		/// <param name="subscriptions">The book subscriptions, where to search.</param>
		/// <param name="personID">The person's id.</param>
		/// <returns>The book subscription.</returns>
		private static BookSubscription GetCurrentPersonSubscription(IEnumerable<BookSubscription> subscriptions, string personID)
		{
			return GetCurrentPersonSubscription(subscriptions, personID, null);
		}

		/// <summary>
		/// Gets the previous or next existing subscription.
		/// </summary>
		/// <param name="book">The current book.</param>
		/// <param name="bookSubscription">The current person subscription.</param>
		/// <param name="isUndo">Indicating that is undo or redo operation.</param>
		/// <returns>The rolled subscription.</returns>
		private BookSubscription GetRollSubscription(Book book, BookSubscription bookSubscription, bool isUndo)
		{
			BookSubscription currentSubscription = GetCurrentPersonSubscription(book, bookSubscription.PersonID, bookSubscription.SubscriptionSet);

			BookSubscription rollbackSubscription = null;

			List<BookSubscription> personSubscriptions =
						book.Subscriptions.Where(subscription => subscription.SubscriptionSet == bookSubscription.SubscriptionSet).OrderBy(subscription => subscription.ID).ToList();

			if (isUndo)
			{
				if (currentSubscription.ParentID == null)
				{
					if (personSubscriptions.Count != 0 && personSubscriptions.Count != 1)
					{
						rollbackSubscription = personSubscriptions[personSubscriptions.Count - 2].ShallowCopy();
					}
				}
				else
				{
					int parentSubscriptionIndex = personSubscriptions.FindIndex(subscription => subscription.ID == currentSubscription.ParentID);

					if (parentSubscriptionIndex != 0)
					{
						rollbackSubscription = personSubscriptions[parentSubscriptionIndex - 1].ShallowCopy();
					}
				}
			}
			else
			{
				if (currentSubscription.ParentID != null)
				{
					int parentSubscriptionIndex = personSubscriptions.FindIndex(subscription => subscription.ID == currentSubscription.ParentID);

					if (parentSubscriptionIndex != personSubscriptions.Count - 1 && personSubscriptions[parentSubscriptionIndex + 1].ParentID == null)
					{
						rollbackSubscription = personSubscriptions[parentSubscriptionIndex + 1].ShallowCopy();
					}
				}
			}

			if (rollbackSubscription != null)
			{
				if (rollbackSubscription.ParentID == null)
				{
					rollbackSubscription.ParentID = rollbackSubscription.ID;
				}

				rollbackSubscription.ID = 0;
			}

			return rollbackSubscription;
		}

		/// <summary>
		/// Chooses the subscription set based on the acion type.
		/// </summary>
		/// <param name="subscription"></param>
		/// <param name="book"></param>
		private string ChooseCorrectSet(Book book, BookSubscription subscription)
		{
			BookSubscription currentSubscription = GetCurrentPersonSubscription(book.Subscriptions, subscription.PersonID, subscription.SubscriptionSet);

			string correctSet = subscription.SubscriptionSet;

			if (subscription.State == (int)SubscribeState.Reader && subscription.SubscriptionSet == null)
			{
				IEnumerable<BookSubscription> waitingList = this.GetWaitingList(book);

				BookSubscription waitingPerson = waitingList.FirstOrDefault(bookSubscription => bookSubscription.PersonID == subscription.PersonID);

				if (waitingPerson != null)
				{
					correctSet = waitingPerson.SubscriptionSet;
				}
				else
				{
					correctSet = Guid.NewGuid().ToString();
				}
			}
			else if (currentSubscription == null && subscription.SubscriptionSet == null)
			{
				correctSet = Guid.NewGuid().ToString();
			}
			else if (subscription.SubscriptionSet == null && currentSubscription != null)
			{
				correctSet = currentSubscription.SubscriptionSet;
			}

			return correctSet;
		}

		#endregion
	}
}
